---
title: "Online Appendix"
subtitle: "Supplementary Materials for BAM and ICA Manuscripts"
author: "Jessala A. Grijalva"
date: "`r format(Sys.Date(), '%B %d, %Y')`"
format:
  pdf:
    toc: true
    toc-depth: 3
    number-sections: true
    geometry: margin=1in
    keep-tex: false
    fig-pos: "H"
    include-in-header:
      text: |
        \usepackage{booktabs}
        \usepackage{caption}
        \captionsetup[table]{labelfont=bf, labelsep=period, justification=raggedright, singlelinecheck=false}
        \captionsetup[figure]{labelfont=bf, labelsep=period, justification=raggedright, singlelinecheck=false}
execute:
  echo: false
  warning: false
  message: false
  error: false
---

```{r setup}
#| label: setup

library(here)
library(pacman)
p_load(dplyr, tidyr, ggplot2, tibble, forcats, knitr, kableExtra, mclust)

knitr::opts_chunk$set(fig.align = "center")

# Load Phase 2 outputs
load(here("data", "processed", "bam_clustered_gmm_k4.rda"))

val_path <- here("data", "processed", "phase2_validation_results.rda")
if (file.exists(val_path)) {
  load(val_path)
  vr <- validation_results
} else {
  stop("phase2_validation_results.rda not found. Re-render Phase_2_Analysis.qmd first.")
}

VARS5 <- vr$VARS5
df <- bam_clustered %>% filter(!is.na(orientation))

# ── Publishing standards ──
VAR_LABELS <- c(
  "AMERICAN"          = "American Identity",
  "CULTURAL_IDENTITY" = "Cultural Identity",
  "KEEPSPAN"          = "Maintain Spanish",
  "DISTINCT"          = "Cultural Distinction",
  "LEARNENG"          = "Learn English"
)

GRAY_PALETTE <- c(
  "Culture Affirming" = "gray15",
  "Assimilationist"   = "gray40",
  "Demicultural"      = "gray65",
  "Bicultural"        = "gray85"
)

SHAPE_PALETTE <- c(
  "Culture Affirming" = 16,
  "Assimilationist"   = 17,
  "Demicultural"      = 15,
  "Bicultural"        = 18
)

has_bt_metrics <- exists("metrics_summary", where = vr)

theme_pub <- function(base_size = 11) {
  theme_minimal(base_size = base_size) %+replace%
    theme(
      text = element_text(color = "black"),
      panel.grid.minor = element_blank(),
      legend.position = "bottom",
      legend.text = element_text(size = 9),
      plot.title = element_blank(),
      plot.subtitle = element_blank()
    )
}

if (!dir.exists(here("figures"))) dir.create(here("figures"), recursive = TRUE)
```

\newpage

# Appendix A: Data and Preprocessing

## A.1 Sample description

The 2006 Latino National Survey (LNS) is a nationally representative
telephone survey of 8,634 self-identified Latino/Hispanic adults across
15 states and Washington, D.C. The analytic sample comprises
`r format(nrow(bam_clustered), big.mark = ",")` respondents who met the
eligibility criteria described in Phase 1 (age $\geq$ 18,
self-identified Latino/Hispanic, nonmissing on all five acculturation
indicators after imputation).

## A.2 Acculturation variables

```{r tbl-vars-description}
#| label: tbl-vars-description

var_desc <- tibble(
  Variable = VAR_LABELS[VARS5],
  `Code Name` = VARS5,
  Description = c(
    "Strength of American identity",
    "Mean of regional + Latino ethnic identity",
    "Importance of maintaining Spanish language",
    "Importance of maintaining distinct cultural practices",
    "Importance of learning English"
  ),
  Scale = c("1--4", "1--4", "1--4", "1--3", "1--4"),
  Dimension = c("American", "Heritage", "Heritage", "Heritage", "American")
)

kable(var_desc,
      caption = "Table A1. Acculturation variable descriptions and theoretical dimension mapping.",
      align = c("l", "l", "l", "c", "c"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

## A.3 Missing data handling

Missing data were imputed using Multivariate Imputation by Chained
Equations with Predictive Mean Matching (MICE-PMM). MICE-PMM preserves
the empirical distribution of each variable (including bounded scales)
and accounts for uncertainty through multiple imputation. Phase 1
documents the complete imputation specification. The canonical imputed
dataset is saved as `clean_data.rda` and is deterministic (Phase 1 is
set to `eval: false` to prevent re-imputation).

## A.4 Descriptive statistics

```{r tbl-descriptives}
#| label: tbl-descriptives

desc_stats <- df %>%
  select(all_of(VARS5)) %>%
  mutate(across(everything(), as.numeric)) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "Value") %>%
  group_by(Variable) %>%
  summarise(
    N    = sum(!is.na(Value)),
    Mean = mean(Value, na.rm = TRUE),
    SD   = sd(Value, na.rm = TRUE),
    Min  = min(Value, na.rm = TRUE),
    Median = median(Value, na.rm = TRUE),
    Max  = max(Value, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(Variable = recode(Variable, !!!VAR_LABELS)) %>%
  mutate(Variable = factor(Variable, levels = VAR_LABELS)) %>%
  arrange(Variable) %>%
  mutate(
    across(c(Mean, SD), ~ sprintf("%.2f", .x)),
    across(c(Min, Median, Max), ~ sprintf("%.0f", .x)),
    N = format(N, big.mark = ",")
  )

kable(desc_stats,
      caption = "Table A2. Descriptive statistics for acculturation variables (post-imputation).",
      align = c("l", "r", "r", "r", "r", "r", "r"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} \textit{N} = 4,785. All variables are ordinal scales. Cultural Identity is the mean of regional and Latino ethnic identity items.

## A.5 Bivariate correlations

```{r tbl-correlations}
#| label: tbl-correlations

cor_mat <- df %>%
  select(all_of(VARS5)) %>%
  mutate(across(everything(), as.numeric)) %>%
  cor(use = "pairwise.complete.obs")

# Lower triangle only
cor_mat[upper.tri(cor_mat)] <- NA
diag(cor_mat) <- NA

cor_df <- as.data.frame(cor_mat) %>%
  mutate(across(everything(), ~ ifelse(is.na(.x), "", sprintf("%.3f", .x))))

rownames(cor_df) <- VAR_LABELS[VARS5]
colnames(cor_df) <- VAR_LABELS[VARS5]

cor_df <- cor_df %>%
  tibble::rownames_to_column("Variable")

kable(cor_df,
      caption = "Table A3. Pairwise Pearson correlations among acculturation variables.",
      align = c("l", rep("r", 5)),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

\noindent \textit{Note:} \textit{N} = 4,785. Lower triangle shown.

\newpage

# Appendix B: Comparative Cluster Analysis Details

## B.1 Algorithm specifications

```{r tbl-algorithm-specs}
#| label: tbl-algorithm-specs

algo_specs <- tibble(
  Algorithm = c("K-Means", "GMM (EEV)", "Fuzzy C-Means",
                "Hierarchical (Ward)", "DBSCAN"),
  Assumption = c(
    "Spherical, equal-size clusters",
    "Elliptical; equal volume and shape, varying orientation",
    "Probabilistic membership (fuzzifier m = 2)",
    "Nested structure from pairwise distances",
    "Density-based; no distributional assumptions"
  ),
  `k Required` = c("Yes", "Yes", "Yes", "Yes",
                     "No (eps, minPts)"),
  Implementation = c("stats::kmeans (50 starts)", "mclust::Mclust",
                      "e1071::cmeans", "stats::hclust + cutree",
                      "dbscan::dbscan (kNN-based eps)")
)

kable(algo_specs,
      caption = "Table A4. Algorithm specifications and implementation details.",
      align = c("l", "l", "c", "l"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

\noindent \textit{Note:} Five algorithms were evaluated at k = 3, 4, and 5. Fuzzy C-Means and DBSCAN were excluded from the final comparison (see Section B.2).

## B.2 Full comparative results

```{r tbl-full-comparative}
#| label: tbl-full-comparative

comp_full <- vr$comp_table %>%
  select(method, k, status, silhouette, ch, sizes) %>%
  arrange(method, k) %>%
  mutate(
    silhouette = ifelse(status == "ok", sprintf("%.2f", silhouette), "--"),
    ch         = ifelse(status == "ok", sprintf("%.2f", ch), "--")
  )

kable(comp_full,
      col.names = c("Algorithm", "k", "Status", "Silhouette", "CH Index",
                     "Cluster Sizes"),
      caption = "Table A5. Full results for all algorithms at k = 3, 4, 5.",
      align = c("l", "r", "c", "r", "r", "l"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

\noindent \textit{Note:} Status indicates whether the algorithm converged at the given k. Silhouette and CH index are reported only for converged solutions. \textit{N} = 4,785.

## B.3 ARI agreement at k = 4

```{r tbl-ari-agreement}
#| label: tbl-ari-agreement

ari_tbl <- vr$ari_table %>%
  mutate(ari_vs_gmm = sprintf("%.2f", ari_vs_gmm))

kable(ari_tbl,
      col.names = c("Algorithm", "ARI vs GMM (k = 4)"),
      caption = "Table A6. Agreement of k = 4 solutions with GMM reference (Adjusted Rand Index).",
      align = c("l", "r"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} ARI ranges from 0 (random agreement) to 1 (perfect agreement), adjusted for chance. Higher values indicate greater convergence with the GMM solution.

\newpage

# Appendix C: Validation Details

## C.1 Bootstrap stability protocol

**Procedure.** For each of B = 1,000 iterations, draw a random 80\%
subsample without replacement, fit GMM (EEV, G = 4), and compute the
Adjusted Rand Index (ARI) against the full-sample cluster assignment.
ARI ranges from 0 (random) to 1 (perfect agreement) and adjusts for
chance.

**Rationale.** Bootstrap stability tests whether the clustering solution
is an artifact of this particular sample or reflects reproducible
structure. An ARI consistently above 0.80 indicates that the same
cluster structure is recovered across most subsamples.

```{r tbl-bootstrap-stability}
#| label: tbl-bootstrap-stability

stab_detail <- tibble(
  Metric = c("Bootstrap iterations (B)",
             "Converged",
             "Failed",
             "Mean ARI",
             "SD ARI",
             "95% CI"),
  Value = c(
    format(vr$stab_summary$B, big.mark = ","),
    format(vr$stab_summary$converged, big.mark = ","),
    as.character(vr$stab_summary$failed),
    sprintf("%.2f", vr$stab_summary$mean_ARI),
    sprintf("%.2f", vr$stab_summary$sd_ARI),
    sprintf("[%.2f, %.2f]", vr$stab_summary$ci_lo, vr$stab_summary$ci_hi)
  )
)

kable(stab_detail,
      col.names = c("Metric", "Value"),
      caption = "Table A7. Bootstrap stability summary (B = 1,000 subsample refits).",
      align = c("l", "r"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} Each iteration draws 80\% of observations without replacement, fits GMM (EEV, G = 4), and computes ARI against the full-sample solution. CI bounds are 2.5th and 97.5th percentiles of converged iterations.

```{r fig-bootstrap-distribution}
#| label: fig-a1-bootstrap
#| fig-cap: "Figure A1. Bootstrap ARI distribution (B = 1,000). Dashed line marks the mean; dotted lines mark the 95 percent CI bounds."
#| fig-width: 6
#| fig-height: 3.5

stab_ok <- vr$stab %>% filter(ok)

ggplot(stab_ok, aes(x = ari)) +
  geom_histogram(bins = 50, fill = "gray40", color = "white") +
  geom_vline(xintercept = vr$stab_summary$mean_ARI,
             linetype = "dashed", color = "black", linewidth = 0.8) +
  geom_vline(xintercept = vr$stab_summary$ci_lo,
             linetype = "dotted", color = "gray30") +
  geom_vline(xintercept = vr$stab_summary$ci_hi,
             linetype = "dotted", color = "gray30") +
  annotate("text", x = vr$stab_summary$mean_ARI + 0.015,
           y = Inf, vjust = 1.5, hjust = 0,
           label = sprintf("Mean = %.2f", vr$stab_summary$mean_ARI),
           size = 3) +
  labs(x = "ARI vs Full-Sample Solution", y = "Count") +
  theme_pub()
```

```{r save-fig-a1}
#| label: save-fig-a1
ggsave(here("figures", "fig_a1_bootstrap_ari.png"),
       width = 6, height = 3.5, units = "in", dpi = 300, bg = "white")
```

## C.2 Bootstrap CIs on silhouette and Dunn index

```{r tbl-bootstrap-metrics}
#| label: tbl-bootstrap-metrics

if (has_bt_metrics) {
  ms <- vr$metrics_summary
  bt_met <- tibble(
    Metric = c("Silhouette", "Dunn Index"),
    Mean   = sprintf("%.2f", c(ms$sil_mean[1], ms$dunn_mean[1])),
    `95 pct CI` = c(
      sprintf("[%.2f, %.2f]", ms$sil_lo[1], ms$sil_hi[1]),
      sprintf("[%.2f, %.2f]", ms$dunn_lo[1], ms$dunn_hi[1])
    )
  )

  kable(bt_met,
        caption = "Table A7b. Bootstrap 95 percent CIs for silhouette and Dunn index (B = 400).",
        align = c("l", "r", "r"),
        booktabs = TRUE) %>%
    kable_styling(latex_options = c("hold_position"), full_width = FALSE)
} else {
  cat("Bootstrap metric CIs not available. Re-render Phase_2_Analysis.qmd to generate them.\n")
}
```

\noindent \textit{Note:} Each iteration drew an 80\% subsample without replacement, fit GMM (EEV, G = 4), and computed silhouette and Dunn index on the subsample solution. CI bounds are 2.5th and 97.5th percentiles.

## C.3 Bootstrap CIs on cluster means

**Procedure.** Resample with replacement B = 400 times, compute
conditional cluster means on VARS5 using the full-sample cluster labels,
extract 2.5th and 97.5th percentiles as 95\% CIs.

```{r tbl-bootstrap-ci-full}
#| label: tbl-bootstrap-ci-full

ci_full <- vr$bt_ci %>%
  mutate(
    formatted = sprintf("%.2f [%.2f, %.2f]", mean, lo, hi),
    variable = recode(variable, !!!VAR_LABELS)
  ) %>%
  select(orientation, variable, formatted) %>%
  pivot_wider(names_from = variable, values_from = formatted)

kable(ci_full,
      col.names = c("Orientation", VAR_LABELS[VARS5]),
      caption = "Table A8. Bootstrap 95 percent CIs for cluster means on all acculturation variables (B = 400).",
      align = c("l", rep("c", 5)),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

\noindent \textit{Note:} Values shown as mean [2.5th, 97.5th percentile]. Resampled with replacement B = 400 times using full-sample cluster labels. \textit{N} = 4,785.

## C.4 Cross-validation details

**Procedure.** Partition the data into 5 folds. For each fold, fit GMM
(EEV, G = 4) on the remaining 4 folds and predict cluster membership for
the held-out fold. Evaluate silhouette and Calinski-Harabasz index on
both training and test sets.

**Rationale.** Cross-validation addresses overfitting by evaluating
whether the model generalizes to unseen data. Consistency across folds
in both silhouette and CH index indicates that the solution is not
overfit to particular observations.

```{r tbl-cv-folds}
#| label: tbl-cv-folds

cv_tbl <- vr$cv_results %>%
  filter(status == "ok") %>%
  select(-status) %>%
  mutate(across(where(is.numeric), ~ sprintf("%.2f", .x)))

kable(cv_tbl,
      col.names = c("Fold", "Silhouette (Train)", "Silhouette (Test)",
                     "CH (Train)", "CH (Test)"),
      caption = "Table A9. Five-fold cross-validation results.",
      align = c("r", rep("r", 4)),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} GMM (EEV, G = 4) fit on training folds (80\%) and predicted on held-out fold (20\%). Silhouette and CH index computed on both sets.

```{r tbl-cv-summary}
#| label: tbl-cv-summary

cv_sum <- tibble(
  Metric = c("Mean silhouette (train)",
             "Mean silhouette (test)",
             "SD silhouette (test)",
             "Mean CH (train)",
             "Mean CH (test)"),
  Value = c(
    sprintf("%.2f", vr$cv_summary$mean_sil_train),
    sprintf("%.2f", vr$cv_summary$mean_sil_test),
    sprintf("%.2f", vr$cv_summary$sd_sil_test),
    sprintf("%.2f", vr$cv_summary$mean_ch_train),
    sprintf("%.2f", vr$cv_summary$mean_ch_test)
  )
)

kable(cv_sum,
      caption = "Table A10. Cross-validation summary statistics.",
      align = c("l", "r"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

```{r fig-cv-comparison}
#| label: fig-a2-cv
#| fig-cap: "Figure A2. Train vs test silhouette across 5 folds. Consistency between train and test indicates the solution generalizes."
#| fig-width: 5
#| fig-height: 3.5

cv_long <- vr$cv_results %>%
  filter(status == "ok") %>%
  select(fold, sil_train, sil_test) %>%
  pivot_longer(-fold, names_to = "Set", values_to = "Silhouette") %>%
  mutate(Set = ifelse(Set == "sil_train", "Train", "Test"))

ggplot(cv_long, aes(x = factor(fold), y = Silhouette, fill = Set)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.6,
           color = "black") +
  scale_fill_manual(values = c("Train" = "gray40", "Test" = "gray80")) +
  labs(x = "Fold", y = "Silhouette", fill = NULL) +
  theme_pub()
```

```{r save-fig-a2}
#| label: save-fig-a2
ggsave(here("figures", "fig_a2_cv_folds.png"),
       width = 5, height = 3.5, units = "in", dpi = 300, bg = "white")
```

\newpage

# Appendix D: Cluster Profile Details

## D.1 Raw means and SDs by orientation

```{r tbl-raw-means}
#| label: tbl-raw-means

raw_long <- df %>%
  select(orientation, all_of(VARS5)) %>%
  mutate(across(all_of(VARS5), as.numeric)) %>%
  pivot_longer(-orientation, names_to = "Variable", values_to = "Value") %>%
  group_by(orientation, Variable) %>%
  summarise(
    M  = sprintf("%.2f", mean(Value, na.rm = TRUE)),
    SD = sprintf("%.2f", sd(Value, na.rm = TRUE)),
    .groups = "drop"
  ) %>%
  mutate(
    cell = paste0(M, " (", SD, ")"),
    Variable = recode(Variable, !!!VAR_LABELS)
  ) %>%
  select(orientation, Variable, cell) %>%
  pivot_wider(names_from = Variable, values_from = cell)

n_by_orient <- df %>%
  count(orientation) %>%
  mutate(n = format(n, big.mark = ","))

raw_display <- left_join(n_by_orient, raw_long, by = "orientation") %>%
  rename(Orientation = orientation)

kable(raw_display,
      caption = "Table A11. Raw means and standard deviations by orientation.",
      align = c("l", "r", rep("c", 5)),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position", "scale_down"),
                full_width = FALSE, font_size = 9)
```

\noindent \textit{Note:} Values shown as M (SD). \textit{N} = 4,785.

## D.2 Orientation profile interpretation guide

```{r tbl-profile-interpretation}
#| label: tbl-profile-interpretation

n_total <- nrow(df)
orient_n <- table(df$orientation)

interp <- tibble(
  Orientation = c("Culture Affirming", "Assimilationist",
                  "Demicultural", "Bicultural"),
  Heritage = c("High", "Low", "Low", "High"),
  American = c("Low", "High", "Low", "High"),
  n = as.integer(orient_n[c("Culture Affirming", "Assimilationist",
                             "Demicultural", "Bicultural")]),
  Pct = sprintf("%.1f", 100 * n / n_total),
  `Binary Model` = c("Heritage-oriented", "American-oriented",
                      "Not captured", "Not captured")
)

kable(interp,
      col.names = c("Orientation", "Heritage", "American", "n", "%", "Binary Model"),
      caption = "Table A12. Orientation profile interpretations.",
      align = c("l", "c", "c", "r", "r", "l"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} Heritage and American dimension levels based on standardized cluster means relative to the sample mean. The binary model can only detect two of the four orientations, missing the 76.3\% of respondents who fall into hybrid categories.

\newpage

# Appendix E: Robustness and Sensitivity

## E.1 Analysis contract

All results are reproducible from the following contract:

```{r analysis-contract}
#| echo: true
#| eval: false

# Analysis Contract
SEED <- 2500L
VARS5 <- c("AMERICAN", "CULTURAL_IDENTITY", "KEEPSPAN",
            "DISTINCT", "LEARNENG")
# Preprocessing: scale(as.matrix(X_raw))
# Model: Mclust(X, G = 4, modelNames = "EEV")
# Bootstrap stability: B = 1000, frac = 0.80, ARI metric
# Bootstrap CIs: B = 400, 95% percentile intervals
# Cross-validation: 5-fold, predict() on held-out folds
```

## E.2 Sensitivity to seed

The seed value (2500) was pre-registered before analysis. GMM with EM
initialization is deterministic conditional on the seed and the data.
The bootstrap stability analysis (B = 1,000) provides evidence that the
solution is not an artifact of the specific initialization.

## E.3 Binary vs. bidirectional model comparison

```{r binary-comparison}
#| label: binary-comparison

n_total <- nrow(df)
n_binary <- sum(df$orientation %in% c("Culture Affirming", "Assimilationist"))
n_hybrid <- sum(df$orientation %in% c("Bicultural", "Demicultural"))

binary_tbl <- tibble(
  Category = c("Binary orientations (Culture Affirming + Assimilationist)",
               "Hybrid orientations (Bicultural + Demicultural)",
               "Total classified"),
  n = c(
    format(n_binary, big.mark = ","),
    format(n_hybrid, big.mark = ","),
    format(n_total, big.mark = ",")
  ),
  Pct = c(
    sprintf("%.1f", 100 * n_binary / n_total),
    sprintf("%.1f", 100 * n_hybrid / n_total),
    "100.0"
  )
)

kable(binary_tbl,
      col.names = c("Category", "n", "%"),
      caption = "Table A13. Binary vs. bidirectional model comparison.",
      align = c("l", "r", "r"),
      booktabs = TRUE) %>%
  kable_styling(latex_options = c("hold_position"), full_width = FALSE)
```

\noindent \textit{Note:} A binary model capturing only Culture Affirming and Assimilationist orientations would fail to classify the majority of respondents. The hybrid orientations require a bidirectional framework.

\newpage

# Appendix F: Software and Reproducibility

## F.1 Software versions

```{r session-info}
#| label: session-info

sessionInfo()
```

## F.2 File chain

The complete analysis pipeline is:

1. **Phase 1** (`Phase_1_Preprocessing.qmd`): Raw LNS data $\rightarrow$
   imputed, cleaned dataset (`clean_data.rda`). Set to `eval: false` to
   preserve canonical imputation.
2. **Phase 2** (`Phase_2_Analysis.qmd`): Comparative cluster analysis,
   GMM k = 4 fit, bootstrap stability, bootstrap CIs, 5-fold CV.
   Outputs `bam_clustered_gmm_k4.rda` and `phase2_validation_results.rda`.
3. **Manuscript Figures** (`Manuscript_Figures.qmd`): Publication-ready
   figures and tables for both papers. Exports standalone PNGs at 300
   and 600 DPI to `figures/`.
4. **Appendix** (this document): Comprehensive supplementary materials
   supporting both manuscripts.

All paths use `here::here()` for portability. The repository root is
defined by the `.Rproj` file.

## F.3 Figure export specifications

All figures are saved as standalone PNG files in `figures/` at two
resolutions:

- **300 DPI**: Standard for manuscript review and most journals.
- **600 DPI**: Available on request for journals requiring higher
  resolution (e.g., production-quality typesetting).

Journals that require vector formats (EPS, PDF) can request them; the
Quarto source renders natively to PDF figures.
